
import base64
import copy
import json
import os
import time
import uuid
from datetime import datetime, timedelta
from io import BytesIO
import sys

import ddddocr
import numpy
import requests
import schedule
from plyer import notification
from PIL import Image, ImageDraw

from yuan_shen import file_util
from yuan_shen import logger_util
from yuan_shen import time_utils
from yuan_shen.ini_util import IniUtil

class YuanShen():
    ini_util = IniUtil()
    config = ini_util.get_all()
    docr = ddddocr.DdddOcr(beta=True, show_ad=False)
    ddet = ddddocr.DdddOcr(det=True, show_ad=False)


    # 全局变量
    LAST_REQUEST_TIME = 0
    HEADERS = {}

    # 用户配置
    # time_dict = {'0': '09:00-10:00', '1': '10:00-11:00', '2': '11:00-12:00', '3': '12:00-13:00', '4': '13:00-14:00',
    #              '5': '14:00-15:00', '6': '15:00-16:00', '7': '16:00-17:00', '8': '17:00-18:00', '9': '18:00-19:00',
    #              '10': '19:00-20:00', '11': '20:00-21:00', '12': '21:00-22:00', }
    # [13, 14, 1, 5, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, ]  # 期望场地

    # 系统常量
    DEPTH_THRESHOLD = 3  # 方法递归深度
    DEPTH_THRESHOLD_VERIFY = 20  # 验证码递归深度
    REDUCER_THRESHOLD = 0.3  # 请求最短间隔时间（秒）
    REDUCER_STEP = 0.03  # 减速带间隔（秒）
    # SMS_FILE = 'resources/data/sms.json'
    # HEADERS_FILE = 'resources/data/headers.json'

    # 系统常量
    TAG = 'DHwhe0n95B'
    BUSINESS_ID = '10000104'
    SYS_ID = '13'
    BUSINESS_TYPE = '1301'
    STADIUM_ID = '10224'
    HOST = 'app.papa.com.cn'

    # 接口状态码
    STATUS_200 = 200  # 成功
    STATUS_4000010036 = 4000010036  # 链接错误
    STATUS_40101 = 40101  # 请重新登陆(token失效)/系统繁忙
    STATUS_40102 = 40102  # 令牌或签名不能为空
    STATUS_40108 = 40108  # 您的动作太快了，请休息一下
    STATUS_40112 = 40112  #
    STATUS_40200 = 40200  # AuthCode过期
    STATUS_40004007 = 40004007  # 场地被其他人占用
    STATUS_40004017 = 40004017  # 订单中心：验价异常报错
    STATUS_40004900 = 40004900  # 已有订单
    STATUS_40004011 = 40004011  # 余额不足
    STATUS_40103 = 40103  # 订场时间
    STATUS_40201 = 40201  # 验证码
    STATUS_4001000006 = 4001000006  # 须知不存在
    STATUS_4000010038 = 4000010038  # 验证码一分钟内只能发送一次
    STATUS_4000010000 = 4000010000  # 登录短信验证码一天最多只能发送10次 / 短信验证码有效时间已过
    STATUS_4000010009 = 4000010009  # 验证码错误
    STATUS_4000010027 = 4000010027  # 验证码错误
    # 接口列表
    URL_GET_APP_ID = 'https://api.wesais.com/member/wxMember/getAppID'  # 获取店铺
    URL_VERIFY_CODE = 'https://api.wesais.com/member/wxMember/verifyCode'  # 获取验证码
    URL_GET_LIST = 'https://api.wesais.com/field/wxFieldBuyPlan/getList'  # 获取场地列表
    URL_SEND_LOGIN_CODE = 'https://api.wesais.com/member/wxMember/sendLoginCode'  # 提交验证码
    URL_LOGIN = 'https://api.wesais.com/member/wxMember/login'  # 登录接口
    URL_GET_TOKEN = 'https://api.wesais.com/member/wxMember/exToken'  # 换取token
    URL_POPUP_ALERT = 'https://api.wesais.com/field/wxFieldBuyPlan/getPoundagePopupAlert'  # 获取手续费提示
    URL_CREATE_ORDER = 'https://api.wesais.com/shop/order/create'  # 创建订单
    URL_CANCEL_ORDER = 'https://api.wesais.com/shop/order/cancel'  # 取消订单
    URL_SEARCH_ORDER = 'https://api.wesais.com/shop/order/list'  # 查询订单
    URL_POUNDAGE = 'https://api.wesais.com/field/wxFieldBuyPlan/getPoundagePop'  # 手续费
    URL_TIME_STAMP = 'https://api.wesais.com/field/businessBase/getNowTimeStamp'  # 获取系统时间


    # 环境设置
    requests.packages.urllib3.disable_warnings()  # 关闭https请求的不安全警告
    request_sess = requests.Session()
    # 报错requests.exceptions.ProxyError: HTTPSConnectionPool(host='www.xxx.com', port=443): Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.
    os.environ['NO_PROXY'] = 'api.wesais.com'  # 关闭对要访问的域名的代理
    logger = logger_util.get_logger()


    class TicketError(Exception):
        pass

    def get_uuid(self) -> str:
        return str(uuid.uuid1()).replace('-', '')

    def get_phone(self) -> str:
        return self.ini_util.get('user', 'phone')

    def get_sms_file(self):
        return self.ini_util.get('constant', 'sms_file')

    def get_headers_file(self):
        return self.ini_util.get('constant', 'headers_file')

    def get_monitor_date(self):
        self.logger.info('==>> 开始读取监控配置')
        monitor = {}
        dates = []
        policy = self.ini_util.get('monitor', 'policy')
        policy_date = self.ini_util.get('monitor', 'policy_date')
        monitor['policy'] = policy
        monitor['policy_date'] = policy_date
        if policy and policy_date:
            policy_date = policy_date.replace(' ', '').split(',')
            if policy == 'date':
                for date in policy_date:  # 过滤无效的时间
                    if date >= time_utils.get_time(format_str="%Y-%m-%d") or date <= time_utils.get_days_after(7, format_str='%Y-%m-%d'):
                        dates.append(date)
            elif policy == 'day':
                policy_date = [int(x) for x in policy_date]
                for i in range(len(policy_date)):
                    dates.append((datetime.today() + timedelta(policy_date[i])).strftime("%Y-%m-%d"))
            elif policy == 'week':
                dates = time_utils.get_date_current_week(*policy_date)
            else:
                self.logger.warn('==>> 监控策略不完整，取当天日期')
                dates.append(datetime.today().strftime("%Y-%m-%d"))
        else:
            self.logger.warn('==>> 监控策略不完整，取当天日期')
            dates.append(datetime.today().strftime("%Y-%m-%d"))
        monitor['dates'] = dates
        self.logger.info(f'==>> 监控配置:{monitor}')
        return monitor

    def get_expect_config(self, date):
        """
        获取指定日期期望时间列表和期望场地列表
        * default和 week域至少有一处配置expect_time和expect_ground
        :param date:
        :param busi:grab 或 monitor
        :return:
        """
        self.logger.debug(f'==> * * * * * * 获取期望时间和场地列表:{date} * * * * * * <<==')
        week = datetime.strptime(date, '%Y-%m-%d').strftime('%A')
        time_section = ground_section = week
        expect_time = self.ini_util.get(week, f'expect_time')
        expect_ground = self.ini_util.get(week, f'expect_ground')
        if not expect_time:
            time_section = 'default'
            expect_time = self.ini_util.get('default', f'expect_time')
        if not expect_ground:
            ground_section = 'default'
            expect_ground = self.ini_util.get('default', f'expect_ground')
        self.logger.debug(f'==>> 时间配置 week:{week} | time_section:{time_section} | expect_time:{expect_time}')
        self.logger.debug(f'==>> 场地配置 week:{week} | ground_section:{ground_section} | expect_ground:{expect_ground}')
        if not expect_time or not expect_ground:
            self.logger.error('配置文件错误')
        expect_time = [int(x) for x in expect_time.replace(' ', '').split(',')]
        expect_ground = [int(x) for x in expect_ground.replace(' ', '').split(',')]
        return expect_ground, expect_time

    def is_login(self) -> bool:
        self.logger.info(f'==>> 是否登录')
        param_time = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'request_id': self.get_uuid(),
        }
        try:
            response = self.request_sess.post(self.URL_TIME_STAMP, param_time, headers=self.HEADERS, verify=False)
        except Exception as e:
            self.logger.error(f'===>>> 请求异常:{e}')
            return False

        if not response.status_code == self.STATUS_200:
            self.logger.error(f'===>>> 接口报错1 response:{response.text}')
            return False
        response_json = response.json()
        if response_json['code'] == self.STATUS_200:
            return True
        elif response_json['code'] == self.STATUS_40101:  # token失效
            self.logger.error(f'===>>> 请重新登陆:{response.text}')
            return self.login()
        elif response_json['code'] == self.STATUS_40102:  # 令牌或签名不能为空
            self.logger.error(f'===>>> 令牌或签名不能为空:{response.text}')
            return self.login()

    def login(self, depth: int = 1) -> bool:
        """
        登陆模块
        1.识别验证码并获取 auth_code；（失败时自己重试）
        2.获取短信验证码，使用验证码和 auth_code 登陆；（失败时重试login方法）
        3.使用 auth_code 换取 token；（失败时重试login方法）
        """
        self.logger.info('==>> 执行登陆模块-开始')
        if depth > self.DEPTH_THRESHOLD:
            self.logger.error(f'===>>> 方法重试次数超过阈值:{depth}/{self.DEPTH_THRESHOLD}')
            return False
        else:
            depth = depth + 1

        auth_code, sms_send_time = self.get_auth_code()
        if not auth_code:
            self.logger.error('获取auth_code失败，请检查')
            self.login(depth)
        account_login = self.login_by_code(auth_code, sms_send_time)  # 登陆-返回true则成功
        if account_login is not True:
            self.logger.error('登录失败，重新登录')
            return self.login(depth)
        token_json = self.get_token(auth_code)  # 登陆成功-换取token
        if not token_json:
            self.logger.error('===>>> 获取token失败')
            return self.login(depth)
        else:
            self.logger.info('==>> 保存token')
            self.HEADERS['Authorization'] = token_json['token']  # 设置token
            with open(self.get_headers_file(), 'w', encoding='utf-8') as file:
                json.dump(self.HEADERS, file, indent=2, ensure_ascii=False, )
        self.logger.info('==>> 执行登陆模块-结束')
        return True

    def get_auth_code(self, depth: int = 0):
        self.logger.info(f"==>> 开始获取auth_code")
        if depth > self.DEPTH_THRESHOLD_VERIFY:
            self.logger.error(f'===>>> 方法重试次数超过阈值:{depth}/{self.DEPTH_THRESHOLD_VERIFY}')
            return False
        else:
            depth = depth + 1

        verify_path = f'resources/material/verify1/tmp/verify_{time_utils.get_time(format_str="%Y%m%d-%H%M%S-%f")}.png'
        verify_id = self.get_verify_img(verify_path)  # 获取验证码
        verify_code = self.get_verify_code(verify_path)  # 解析验证码

        self.logger.info('==>> 开始发送登陆验证码')
        param_send_code = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'phone': self.get_phone(),
            'auth_code': '',
            'verify_code': verify_code,
            'verify_id': verify_id,
            'tag': self.TAG,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_SEND_LOGIN_CODE, param_send_code)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return self.get_auth_code(depth + 1)
        if response_json['code'] == self.STATUS_4000010009:
            self.logger.error(f'===>>> 验证码错误，重试')
            os.renames(verify_path,
                      f'resources/material/verify1/err/{time_utils.get_time(format_str="%Y%m%d-%H%M%S-%f")}_{verify_code}.png')  # 识别错误，移动到错误文件夹
            return self.get_auth_code(depth + 1)
        if response_json['code'] == self.STATUS_4000010038:
            self.logger.error(f'===>>> 验证码一分钟内只能发送一次')
            time.sleep(60)
            return self.get_auth_code(depth + 1)
        verify_path_new = f'resources/material/verify1/{time_utils.get_time(format_str="%Y%m%d-%H%M%S-%f")}_{verify_code}.png'
        os.renames(verify_path, verify_path_new)  # 识别正确,名称改为验证码
        auth_code = response_json['data']['auth_code']
        self.logger.info(f"==>> 获取到 auth_code:{auth_code}")

        return auth_code, time_utils.get_time(to_string=False)

    def get_verify_img(self, verify_path):
        self.logger.info('==>> 开始获取登陆验证码')

        param_verify = {
            'business_id': self.BUSINESS_ID,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_VERIFY_CODE, param_verify)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        verify_str = response_json['data']['image']
        verify_id = response_json['data']['verify_id']
        file_util.create_not_exist(verify_path)
        with open(verify_path, 'wb') as f:
            f.write(base64.b64decode(verify_str.split(',', 1)[1]))
        self.logger.info(f'==>> 验证码路径:{verify_path}')
        return verify_id

    def get_verify_code(self, verify_path):
        self.logger.info('==>> 开始解析登陆验证码')
        ocr = ddddocr.DdddOcr(show_ad=False)
        with open(verify_path, 'rb') as f:
            image = f.read()
        res = ocr.classification(image)
        self.logger.info(f'==>> 验证码解析结果：{res}')
        return res

    def send_login_code(self, verify_code, verify_id):
        self.logger.info('==>> 开始发送登陆验证码')
        param_send_code = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'phone': self.get_phone(),
            'auth_code': '',
            'verify_code': verify_code,
            'verify_id': verify_id,
            'tag': self.TAG,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_SEND_LOGIN_CODE, param_send_code)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json['code'] == self.STATUS_4000010009:
            self.logger.error(f'===>>> 验证码错误 重试')
            return None
        auth_code = response_json['data']['auth_code']
        self.logger.info(f"==>> 获取到 auth_code:{auth_code}")
        return auth_code

    def login_by_code(self, auth_code, sms_send_time):
        """
        使用auth_code和短信验证码登陆系统（短信验证码在方法内获取）
        :param auth_code: auth_code
        :param sms_send_time: 验证码发送时间（日期格式）
        :return:
        """
        self.logger.info('==>> 开始登陆')

        #  读取验证码
        sms_code = None
        # sms_send_time = datetime.strptime(sms_send_time, '%Y-%m-%d %H:%M:%S')  # 验证码发送时间
        sms_fail_time = sms_send_time + timedelta(minutes=5)  # 验证码失效时间
        while True:
            try:
                if time_utils.get_time(to_string=False) > sms_fail_time:
                    self.logger.error('===>>> 验证码已过期，请重新获取验证码')
                    return False  # 验证码已过期，重新执行 login()
                if not sms_code:
                    with open(self.get_real_path(self.get_sms_file()), 'r', encoding='utf-8') as file:
                        sms = json.load(file)
                    self.logger.debug(f'==>> sms:{sms}')
                    if sms:
                        sms_code_tmp = sms.get('sms_code')
                        sms_receive_time = sms.get('sms_date')  # 验证码接收时间，手动填写时设置为sysdate可跳过接收时间校验
                        if sms_receive_time == 'sysdate' or sms_send_time < sms_receive_time:
                            self.logger.info(f'==>> 获取到验证码 sms_code_tmp[{sms_code_tmp}]')
                            sms_code = sms_code_tmp
                            break
                        else:
                            self.logger.error(f'===>>> 无效验证码 sms_send:{sms_send_time}, sms_receive_time:{sms_receive_time} sms_code_tmp:{sms_code_tmp}')
                else:
                    pass  # 不会进入这里
            except Exception as e:
                self.logger.error(f'===>>> 读取验证码异常：{e}')
            time.sleep(20)

        # sms_code = input("请输入短信验证码：")
        param_login = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'phone': self.get_phone(),
            'host': self.HOST,
            'sms_code': sms_code,
            'auth_code': auth_code,
            'tag': self.TAG,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_LOGIN, param_login)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json['code'] == self.STATUS_4000010027:  # 验证码错误，重新输入
            self.logger.error('===>>> 验证码错误')  # 验证码错误，重新执行 login()
            return False
        if response_json['code'] == self.STATUS_4000010000:
            self.logger.error('===>>> 登录短信验证码一天最多只能发送10次 / 短信验证码有效时间已过')  # 结束程序，第二天再试
            if '一天最多只能发送' in response_json['message']:
                sys.exit(0)
            return False

        account_login = response_json['data']['account_login']  # true
        # auth_code = response_login_json['data']['auth_code']
        # business_id = response_login_json['data']['business_id']
        # is_blacklist = response_login_json['data']['is_blacklist']  # 0
        # wx_login = response_login_json['data']['wx_login']  # false
        return account_login

    def get_token(self, auth_code):
        """
        登陆成功后，使用auth_code换取token
        :param auth_code:
        :return:
        """
        self.logger.info('==>> 开始换取token')
        param_token = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'auth_code': auth_code,
            'tag': self.TAG,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_GET_TOKEN, param_token)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        account_id = response_json['data']['account_id']  # 530422
        account_type = response_json['data']['account_type']  # 31
        # business_id = response_json['data']['business_id']  # 10000104
        login_info = response_json['data']['login_info']  # 空
        token = response_json['data']['token']  # xxxxxxxxxxxxxxxx
        union_token = response_json['data']['union_token']  # 空
        return response_json['data']

    def get_ground_list(self, time_date, depth: int = 0):
        """
        根据日期获取场地列表
        :param time_date: 日期（yyyy-mm-dd）
        :param depth: 重试次数
        :return: 20个场地数据
        """
        # time_date = '2023-08-02'
        self.logger.info(f'==>> * * * * * * 开始获取场地列表{time_date} * * * * * * <<==')
        param_list = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'ground_id': '10224001',
            'time_date': time_date,
            'request_id': self.get_uuid(),
        }
        time1 = time_utils.get_time(0, 0)
        response_json1 = self.send_request(self.URL_GET_LIST, param_list)
        if not response_json1:
            self.logger.error('===>>> 请求错误')
            return False

        param_list['ground_id'] = '10224002'
        param_list['request_id'] = self.get_uuid()
        response_json2 = self.send_request(self.URL_GET_LIST, param_list)
        if not response_json2:
            self.logger.error('===>>> 请求错误')
            return False

        response_json1['data']['fieldStr'][10: 10] = response_json2['data']['fieldStr']
        response_json1['data']['skuList'][10: 10] = response_json2['data']['skuList']
        return response_json1


    def get_target_ground_by_time(self, ground_response_list, expect_ground, expect_time):
        """
        筛选期望场地
        1.根据时间期望表和场地期望表获取场地信息；
        2.时间期望表一般不包含所有时间，场地期望表一般包含所有场地，根据期望排序；
        3.如果时间期望内没有可选场地，则无法得到场地；如果时间期望内有场地，则根据场地期望肯定可以得到场地；
        :param expect_time:
        :param expect_ground:
        :param ground_response_list:
        :return:
        """
        self.logger.info('==>> * * * * * * 开始筛选期望场地 * * * * * * <<==')
        if not ground_response_list:
            return False
        data = ground_response_list['data']
        sku_list = data['skuList']
        for ground_idx in expect_ground:
            if (ground_idx - 1) > (len(sku_list) - 1):
                self.logger.error(f'===>>> 当前场地号异常{ground_idx}')
                continue
            sku_day = sku_list[ground_idx - 1]
            sku_day = numpy.array(sku_day).reshape(-1)
            for time_idx in expect_time:
                if time_idx > (len(sku_day) - 1):
                    self.logger.error(f'===>>> 当前时间段异常{time_idx}')
                    continue
                sku = sku_day[time_idx]
                if sku and not sku['is_lock']:  # is_lock=False表示未锁
                    self.logger.info(f'==>> 期望场地:{sku}')
                    return sku


    def return_last_value(self, retry_state):
        self.logger.error("===>>> 所有重试失败后执行回调函数\n")
        return retry_state.outcome.result()  # 表示返回原函数的返回值


    # @retry(
    #     stop=stop_after_attempt(3),  # 重试指定次数
    #     # wait=wait_fixed(3) + wait_random(0, 2),  # 重试前等待固定3秒并加上0到2秒的随机抖动
    #     retry=retry_if_result(lambda x: x is False) | retry_if_exception_type(),  # 组合重试条件
    #     # before=retry_before,  # 重试之前调用，即使没重试过也会进入，每次重试也会进入
    #     # retry_error_callback=return_last_value,  # 所有重试失败后调用自定义回调函数
    #     reraise=True,  # 最后一次重试之后还是失败以后抛出函数原来的异常，而不是RetryError
    # )
    def send_request(self, url, param, headers={}) -> dict:
        """
        # 如果返回值为False时，接口自动重试，最多三次
        :param url:
        :param param:
        :param headers:
        :return: False/None/dict
        """
        self.reducer()
        self.logger.debug(f'==>> 请求地址:{url}')
        self.logger.debug(f'==>> 请求参数:{param}')
        if not headers:
            headers = self.HEADERS
        try:
            response = self.request_sess.post(url, param, headers=headers, verify=False)  # 复用session
            total_seconds = response.elapsed.total_seconds()  # 获取实际的响应时间
            self.logger.debug(f'==>> 请求耗时:{total_seconds}')
        except Exception as e:
            self.logger.error(f'===>>> 请求异常:{e}')
            return False
        if not response.status_code == self.STATUS_200:
            self.logger.error(f'===>>> 接口报错1 response:{response.text}')
            return False

        response_json = response.json()
        # res_size = response.headers.get('Content-Length')
        # logger.info(f'==>> 响应大小:{f"{res_size}B" if res_size else f"{len(response.text)}char"}')
        # logger.info(f'==>> 请求结果:{response_json if len(response.text)<2000 else response.text[:600]}')
        self.save_request(url, param, headers, response_json, total_seconds)
        if response_json['code'] == self.STATUS_200:
            return response_json
        elif response_json['code'] == self.STATUS_40101:  # token失效
            self.logger.error(f'===>>> 请重新登陆:{response.text}')
            self.login()
            return self.send_request(url, param, headers)
        elif response_json['code'] == self.STATUS_40102:  # 令牌或签名不能为空
            self.logger.error(f'===>>> 令牌或签名不能为空:{response.text}')
            self.login()
            return self.send_request(url, param, headers)
        else:
            return response_json

    def save_request(self, url, param, headers, response_json, seconds):
        request_path = file_util.create_not_exist(f'resources/request/{time_utils.get_time(format_ms=0, format_str="%Y%m%d-%H%M%S")}_{param.get("request_id")}.json')
        self.logger.info(f'==>> 请求数据: {request_path}')
        try:
            if param.get('request_id'):
                request_data = {
                    'time': time_utils.get_time(format_ms=0),
                    'seconds': seconds,
                    'url': url,
                    'param': param,
                    'headers': headers,
                    'response': response_json
                }
                with open(request_path, 'w', encoding='utf-8') as file:
                    json.dump(request_data, file,  indent=2, ensure_ascii=False)
            else:
                self.logger.error('===>>> 无request_id')
        except Exception as e:
            self.logger.error(f'===>>> 保存请求数据异常:{e}')

    def reducer(self, reducer_threshold=REDUCER_THRESHOLD):
        """
        减速带
        :return:
        """
        time1 = time_utils.get_time(0, 0)
        if time1 - self.LAST_REQUEST_TIME <= reducer_threshold:
            sleep_info = f'进入减速带: time1:{format(self.LAST_REQUEST_TIME, ".3f")} | '
            while True:
                now = time_utils.get_time(0, 0)
                sleep_info += f'time2:{format(now, ".3f")}, {format(now-self.LAST_REQUEST_TIME, ".3f")}, '
                if now - self.LAST_REQUEST_TIME <= reducer_threshold:
                    sleep_info += f'sleep{reducer_threshold} | '
                    time.sleep(self.REDUCER_STEP)
                else:
                    sleep_info += f'break | '
                    break
                self.logger.debug(f'==> {sleep_info}')
        self.LAST_REQUEST_TIME = time1

    def get_ticket_by_date(self, date):
        """
        根据日期查询余票，如果有符合的余票则下单
        :param date:
        :return:
        """
        response_list = self.get_ground_list(date)  # 获取场地列表
        if not response_list:
            self.logger.error('===>>> 获取场地列表失败')
            return False
        expect_ground, expect_time = self.get_expect_config(date)  # 获取配置
        sku = self.get_target_ground_by_time(response_list, expect_ground, expect_time)  # 筛选目标场地
        if sku:
            status, message = self.create_order(sku['sku'])  # 下单
            if status and message:
                self.send_notice('抢票提醒', message)
                return True
            else:
                self.logger.error(f'===>>> 下单失败:{message}')
                return False
        else:
            self.logger.info(f'==>> 暂无余票')
            return False

    def grab_tickets(self, date=''):
        """
        抢票，一般只有出票当天需要抢票
        07:00:00-7:00:30 循环查询余票
        注：只有在 06:59:00-07:00:30 之间调度本方法，最好在 07:00:00 之前启动以防需要创建 session
        :param date: 日期 yyyy-mm-dd，默认值七天后
        :return:
        """
        date_def = (datetime.today() + timedelta(days=7)).strftime('%Y-%m-%d')
        if date:
            if date < time_utils.get_time(format_str='%Y-%m-%d') or date > date_def:
                self.logger.error(f'===>>> 不在售票日期[{date}]')
                return False
        else:
            date = date_def
        self.logger.info(f'==>> * * * * * * 开始抢票:{date} * * * * * * <<==')
        session_flag = False

        #  持续查询余票，每秒一次
        while True:
            now = time.strftime("%H:%M:%S", time.localtime())
            self.logger.info(f'==>> 当前时间:{now}')
            if '07:00:00' <= now <= '07:00:30':
                self.get_ticket_by_date(date)
                # time.sleep(1)  # 一轮结束不再等待，请求间隔由reducer()方法控制
            elif '06:59:00' <= now < '07:00:00':
                if session_flag is False:
                    self.logger.info('==>> 创建session')
                    self.is_login()
                    session_flag = True
                else:
                    time.sleep(1)
            else:
                self.logger.info('==>> * * * * * * 不在抢票时间，结束抢票任务 * * * * * * <<==\n')
                return False
        return True

    def remaining_ticket_monitor(self):
        """
        余票监控
        07:00:00-21:30:00每分钟查询一次1
        :param date: 日期
        :return: 场地信息或空
        """
        # date = '2023-08-09'
        self.logger.info(f'==>> * * * * * * 开始余票监控 * * * * * * <<==')
        monitor = self.get_monitor_date()  # 读取监控日期配置
        dates = monitor['dates']

        #  持续查询余票，每分钟一次
        while True:
            now = time_utils.get_time(format_str='%H:%M:%S')
            self.logger.info(f'==>> 当前时间:{now} | 日期列表:{dates}')
            if '07:00:00' <= now <= '21:30:00':
                for i in range(len(dates)-1, -1, -1):
                    date = dates[i]
                    result = self.get_ticket_by_date(date)
                    if result:
                        dates.remove(date)
                self.logger.info('==>> 1分钟后再次查询余票\n')
                time.sleep(60)
            else:
                self.logger.info('==>> * * * * * * 不在售票时间，结束监控 * * * * * * <<==\n')
                return

        return True

    def create_order(self, sku_id, headers_dot: dict = None, depth: int = 0, **kwargs):
        """
        todo 如果验证码正确率低可以考虑多线程下单(创建订单至少请求两次接口一共0.4s，6个字0.154s，两个字0.078，四个字0.162s)
        根据场地号创建订单
        如果CaptchaDots正确，则创建订单；如果CaptchaDots不正确，返回新的验证码
        :param sku_id: 场地号
        :param headers_dot: 验证码点击坐标
        :param depth: 递归深度，验证码允许错误的次数
        :return: status 下单结果，message 下单信息
        """
        self.logger.info('==>> * * * * * * 开始创建订单 * * * * * * <<==')
        param_create_order = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'sys_id': self.SYS_ID,
            'sku_slice': f'{sku_id}:1',
            'business_type': self.BUSINESS_TYPE,
            'order_from': '2',
            'handle_info': '{"date_str":""}',
            'sales_id': '0',
            'request_id': self.get_uuid(),
        }
        if not headers_dot:
            headers_dot = copy.copy(self.HEADERS)
        response_json = self.send_request(self.URL_CREATE_ORDER, param_create_order, headers=headers_dot)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json:
            code = response_json.get('code')
            if code == self.STATUS_40201:  # 返回验证码
                self.logger.info('==>> 验证码数据')
                image_str = response_json['data']['image_base64']
                thumb_str = response_json['data']['thumb_base64']

                verify_name = f'verify_{time.strftime("%Y%m%d%H%M%S")}'
                image_path = 'resources/material/verify2/tmp/' + verify_name + '_image.jpg'
                thumb_path = 'resources/material/verify2/tmp/' + verify_name + '_thumb.png'
                file_util.create_not_exist(image_path)
                file_util.create_not_exist(thumb_path)
                with open(image_path, 'wb') as f:
                    f.write(base64.b64decode(image_str.split(',', 1)[1]))
                with open(thumb_path, 'wb') as f:
                    f.write(base64.b64decode(thumb_str.split(',', 1)[1]))
                thumb_code = self.get_thumb_code(thumb_path, image_path)
                thumb_code = [str(i) for i in thumb_code]
                headers_dot['CaptchaDots'] = ','.join(thumb_code)

                if depth < self.DEPTH_THRESHOLD_VERIFY:  # 是否递归
                    return self.create_order(sku_id, headers_dot, depth + 1)
                else:
                    self.logger.error(f'===>>> 下单失败，方法重试次数超过阈值{depth}/{self.DEPTH_THRESHOLD_VERIFY}')
                    return False, '下单失败，验证码错误次数过多！'
            elif code == self.STATUS_200:  # 下单成功
                self.logger.info(f'==>> 订单生成成功！')
                parent_order_no = response_json['data']['list']['order']['parent_order_no']  # 订单编号
                c_time = response_json['data']['list']['order']['c_time']  # 订单时间
                sale_price = response_json['data']['list']['order']['sale_price']  # 定价
                order_price = response_json['data']['list']['order']['order_price']  # 订单金额
                pay_price = response_json['data']['list']['order']['pay_price']  # 支付金额

                stadium_name = response_json['data']['list']['extra']['stadium_name']  # 场馆名称
                user_info = response_json['data']['list']['extra']['user_info']  # 预订人信息

                sku_info = ''  # 订单信息
                item = response_json['data']['list']['item']  # 订单信息
                for val in item:
                    sku_info = f'{sku_info}\n{val["sku_info"]}'
                return True, f'订单编号： {parent_order_no}\n' \
                             f'订单信息： {sku_info}\n' \
                             f'场馆名称： {stadium_name}\n' \
                             f'预订人信息：{user_info}\n' \
                             f'订单时间： {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(c_time))}\n' \
                             f'金额：   ￥{pay_price}'
            elif code == self.STATUS_40004007:  # 场地被其他人占用
                return False, '场地被其他人占用'
            elif code == self.STATUS_40004900:  # 已有订单，请先支付或取消订单
                return True, '已有订单，请先支付或取消订单'
            else:
                self.logger.error('===>>> 未定义状态')
                return False, '未定义状态'
        else:  # 接口异常
            self.logger.error('===>>> 下单失败，下单接口异常！')
            if depth < self.DEPTH_THRESHOLD_VERIFY:  # 是否递归
                return self.create_order(sku_id, headers_dot, depth + 1)
            else:
                self.logger.error(f'===>>> 下单失败，方法重试次数超过阈值{depth}/{self.DEPTH_THRESHOLD_VERIFY}')
                return False, '下单失败，下单接口异常！'

    def cancel_order(self, parent_order_no):
        """
        取消订单
        :param parent_order_no: 订单号
        :return:
        """
        param_cancel = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'parent_order_no': parent_order_no,
            'sales_id': '0',
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_CANCEL_ORDER, param_cancel)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json and response_json.get('code'):
            self.logger.info('==>> 取消成功！')
            return True
        else:
            self.logger.info('==>> 取消失败！')
            return False

    def search_order(self, order_status=-1, page=1, size=8, sort=-1):
        """
        查询订单
        :param order_status:订单状态 -1全部，0未支付，1已支付，2已取消，3已过期，6取消/退款
        :param page:页数
        :param size:页面大小
        :param sort:排序
        :return:
        """
        param_search = {
            'business_id': self.BUSINESS_ID,
            'order_from': 2,
            'order_status': order_status,
            'sort': sort,
            'page': page,
            'size': size,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_SEARCH_ORDER, param_search)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json and response_json.get('data', {}).get('count', 0):
            self.logger.info(response_json)
            return response_json
        else:
            self.logger.info('==>> 无订单信息')
            return None

    def get_poundage(self, sku_ids):
        """
        根据场地号查询手续费
        :param sku_ids: 场地号
        :return:
        """
        param_poundage = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'skus': sku_ids,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_POUNDAGE, param_poundage)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        if response_json['data']['is_poundage']:
            hour = response_json['data']['hour']
            refund_rate = response_json['data']['refund_rate']

    def get_time_stamp(self):
        """
        获取系统时间
        :return:
        """
        self.logger.info('==>> * * * * * * 获取系统时间 * * * * * * <<==')
        param_time = {
            'business_id': self.BUSINESS_ID,
            'stadium_id': self.STADIUM_ID,
            'request_id': self.get_uuid(),
        }
        response_json = self.send_request(self.URL_TIME_STAMP, param_time)
        if not response_json:
            self.logger.error('===>>> 请求错误')
            return False
        time_now = response_json['data']['time_now']
        struct_time = time.localtime(time_now)
        sys_date = time.strftime("%Y%m%d%H%M%S", struct_time)
        loc_date = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S')
        self.logger.info(f'sys_date:{sys_date}')
        self.logger.info(f'loc_date:{loc_date}')

    def get_thumb_code(self, thumb_path, image_path):
        """
        获取点选类验证码的点击坐标
        :param thumb_path: 需要点击的文字
        :param image_path: 背景图
        :return: 点击坐标（X1,Y1,X2,Y2...）
        """
        # thumb_path = "resources/material/verify2/tmp/verify_20230803105750thumb.png"
        # image_path = "resources/material/verify2/tmp/verify_20230803105750image.jpg"
        self.logger.info(f'开始识别验证码：{thumb_path}、{image_path}')
        t1 = time_utils.get_time(0, 0)
        self.logger.debug(f'==>> ***************** t1:{t1}')

        with open(thumb_path, 'rb') as f:
            thumb_img = f.read()
        target_words = self.docr.classification(thumb_img)
        self.logger.info(f'点击文字：{target_words}')
        t2 = time_utils.get_time(0, 0)
        self.logger.debug(f'==>> ***************** t2:{t2} | t2-t1:{format(t2-t1, ".4f")}')


        with open(image_path, 'rb') as f:
            image = f.read()
        poses = self.ddet.detection(image)
        t3 = time_utils.get_time(0, 0)
        self.logger.debug(f'==>> ***************** t3:{t3} | t3-t2:{format(t3-t2, ".4f")}')

        img = Image.open(BytesIO(image))
        draw = ImageDraw.Draw(img)
        click_identify_result = {}
        for row in poses:
            # 框字
            row = (row[0] - 3, row[1] - 3, row[2] + 3, row[3] + 3)
            x1, y1, x2, y2 = row
            draw.line(([(x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1)]), width=1, fill='red')
            # 裁剪出单个字
            corp = img.crop(row)
            img_byte = BytesIO()
            corp.save(img_byte, 'png')
            # 识别出单个字
            word = self.docr.classification(img_byte.getvalue())
            click_identify_result[word] = row
            self.logger.debug(f'==>> ***************** tx:{time_utils.get_time(0, 0)}')
        # img.show()  # 展示一下框字后的图片
        t4 = time_utils.get_time(0, 0)
        self.logger.debug(f'==>> ***************** t4:{t4} | t4-t3:{format(t4-t3, ".4f")}')

        # 计算文字点击坐标
        img_xy = {}
        for key, xy in click_identify_result.items():
            if key:
                img_xy[key] = (int((xy[0] + xy[2]) / 2), int((xy[1] + xy[3]) / 2))
        self.logger.info(f'文字与坐标：{img_xy}')

        # 计算最终点击顺序与坐标
        result = {}
        thumb_code = []
        for word in target_words:
            coordinate = img_xy.get(word, (88, 88))
            result[word] = coordinate
            thumb_code.append(coordinate[0])
            thumb_code.append(coordinate[1])
        self.logger.info(f'点击结果：{result}')
        t5 = time_utils.get_time(0, 0)
        self.logger.debug(f'==>> ***************** t5:{t5} | t5-t4:{format(t5-t4, ".4f")}')

        if not thumb_code:
            self.logger.info('==>> 点击结果为空，设置默认值')
            thumb_code = [88, 88]
        return thumb_code


    def send_notice(self, title, message):
        """
        发送桌面通知
        :param title:
        :param message:
        :return:
        """
        self.logger.info('==>> * * * * * * 发送桌面通知 * * * * * * <<==')
        notification.notify(
            title=title,
            message=message,
            app_icon='resources/img/dolphin.ico',
            timeout=20,
        )

    def get_real_path(self, path):
        """
        根据运行环境和文件相对路径获取文件绝对路径（相对于工作目录）
        :param path: 文件相对路径
        :return: 文件绝对路径
        """
        if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
            bundle_dir = sys._MEIPASS
        else:
            # bundle_dir = os.path.dirname(os.path.abspath(__file__))
            bundle_dir = os.getcwd()
        target_path = os.path.join(bundle_dir, path)
        self.logger.info(f'==>> 目标路径:{target_path}')
        return target_path

    def clear_data(self):
        """
        清理过期数据
        清理request下过多数据，验证码数据
        :return:
        """
        self.logger.info('==>> 清理数据')
        pass


    def __init__(self):
        self.logger.info('==>> * * * * * * 初始化数据 begin * * * * * * <<==')

        if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
            self.logger.info(f'当前环境是:   PyInstaller捆绑包')
            bundle_dir = sys._MEIPASS
        else:
            self.logger.info(f'当前环境是:   Python进程')
            bundle_dir = os.path.dirname(os.path.abspath(__file__))
        self.logger.info(f'捆绑包路径:   {bundle_dir}', )
        self.logger.info(f'程序路径：    {sys.argv[0]}')
        self.logger.info(f'python路径:   {sys.executable}')
        self.logger.info(f'工作路径:     {os.getcwd()}')
        self.logger.info(f'初始化HEADERS')
        with open(self.get_real_path(self.get_headers_file()), 'r', encoding='utf-8') as file:
            self.HEADERS = json.load(file)
        self.logger.info(f'初始化SMS文件')
        file_util.create_not_exist(self.get_sms_file())  # 初始化验证码文件
        with open(self.get_sms_file(), 'w') as file:
            file.write('{}')
        self.clear_data()
        self.logger.info('==>> * * * * * * 初始化数据 end * * * * * * <<==')


    # 单次预定最多两小时
    # 非售票时间查询的场地信息都是锁定
    # 有待支付订单不能再下单
    # 有待支付订单时，在输入验证码之后提示
    # 每天凌晨登录失效
    # *请求太快可能会登录失效，40101
    # 30天最多退票3次
    def main(self):
        # self.init()
        # date = input('输入日期：')  # '2023-08-08'
        # date = '2023-08-25'
        # sku = remaining_ticket_monitor(date)

        # schedule.every().day.at("00:01:00").do(is_login).tag('is_login').run()  # 查看token是否失效
        schedule.every(10).minutes.do(self.is_login).tag('is_login').run()  # 查看token是否失效
        schedule.every().day.at("07:01:00").do(self.remaining_ticket_monitor).tag('remaining_ticket_monitor').run()  # 余票监控
        schedule.every().day.at("06:59:30").do(self.grab_tickets).tag('grab_tickets').run()  # 抢票
        # schedule.every().day.at("06:59:30").do(run_threaded, grab_tickets, date).tag('grab_tickets')  # 抢票 独立线程
        while True:
            schedule.run_pending()
            # logger.info(schedule.jobs)
            time.sleep(1)


if __name__ == "__main__":
    yuan_shen = YuanShen()
    yuan_shen.main()
    # test()

"""
orc: https://www.cnblogs.com/kerlomz/p/13033262.html
     https://zhuanlan.zhihu.com/p/364803133
LOGGER 不记录报错栈信息
"""